package run.halo.app.service.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.event.logger.LogEvent;
import run.halo.app.event.post.SiteUpdatedEvent;
import run.halo.app.event.post.SiteVisitEvent;
import run.halo.app.exception.BadRequestException;
import run.halo.app.exception.NotFoundException;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Content;
import run.halo.app.model.entity.Site;
import run.halo.app.model.entity.SiteCategory;
import run.halo.app.model.entity.SiteTag;
import run.halo.app.model.entity.Tag;
import run.halo.app.model.enums.LogType;
import run.halo.app.model.enums.SiteStatus;
import run.halo.app.model.params.SiteQuery;
import run.halo.app.model.vo.SiteDetailVO;
import run.halo.app.repository.SiteRepository;
import run.halo.app.service.CategoryService;
import run.halo.app.service.OptionService;
import run.halo.app.service.SiteCategoryService;
import run.halo.app.service.SiteService;
import run.halo.app.service.SiteTagService;
import run.halo.app.service.TagService;
import run.halo.app.service.assembler.SiteAssembler;
import run.halo.app.service.base.AbstractCrudService;
import run.halo.app.utils.ServiceUtils;

import static org.springframework.data.domain.Sort.Direction.DESC;

/**
 * Site service implementation.
 *
 * @author meazty
 * @since 2022/7/30 15:38
 **/
@Slf4j
@Service
public class SiteServiceImpl extends AbstractCrudService<Site, Integer> implements SiteService {

    private final SiteAssembler siteAssembler;

    private final SiteRepository siteRepository;

    private final TagService tagService;

    private final CategoryService categoryService;

    private final SiteTagService siteTagService;

    private final SiteCategoryService siteCategoryService;

    private final ApplicationEventPublisher eventPublisher;

    private final OptionService optionService;

    private final ApplicationContext applicationContext;

    public SiteServiceImpl(
        OptionService optionService,
        SiteAssembler siteAssembler,
        SiteRepository siteRepository,
        TagService tagService,
        CategoryService categoryService,
        SiteTagService siteTagService,
        SiteCategoryService siteCategoryService,
        ApplicationEventPublisher eventPublisher,
        ApplicationContext applicationContext) {
        super(siteRepository);
        this.siteAssembler = siteAssembler;
        this.siteRepository = siteRepository;
        this.tagService = tagService;
        this.categoryService = categoryService;
        this.siteTagService = siteTagService;
        this.siteCategoryService = siteCategoryService;
        this.eventPublisher = eventPublisher;
        this.optionService = optionService;
        this.applicationContext = applicationContext;
    }

    @Override
    public Page<Site> pageBy(SiteQuery siteQuery, Pageable pageable) {
        Assert.notNull(siteQuery, "Site query must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        // Build specification and find all
        return siteRepository.findAll(buildSpecByQuery(siteQuery), pageable);
    }

    @Override
    public Page<Site> pageBy(String keyword, Pageable pageable) {
        Assert.notNull(keyword, "keyword must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        SiteQuery siteQuery = new SiteQuery();
        siteQuery.setKeyword(keyword);
        siteQuery.setStatuses(Set.of(
            SiteStatus.PUBLISHED, SiteStatus.DRAFT, SiteStatus.DISABLE, SiteStatus.INTIMATE
        ));

        // Build specification and find all
        return siteRepository.findAll(buildSpecByQuery(siteQuery), pageable);
    }

    @Override
    public Page<Site> pageLatest(int top) {
        Assert.isTrue(top > 0, "Top number must not be less than 0");

        PageRequest latestPageable = PageRequest.of(0, top, Sort.by(DESC, "createTime"));

        return listAll(latestPageable);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SiteDetailVO createBy(Site siteToCreate, Set<Integer> tagIds, Set<Integer> categoryIds,
        boolean autoSave) {
        SiteDetailVO createdSite = createOrUpdate(siteToCreate, tagIds, categoryIds);
        if (!autoSave) {
            // Log the creation
            LogEvent logEvent = new LogEvent(this, createdSite.getId().toString(),
                LogType.SITE_PUBLISHED, createdSite.getName());
            eventPublisher.publishEvent(logEvent);
        }
        return createdSite;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SiteDetailVO updateBy(Site siteToUpdate, Set<Integer> tagIds, Set<Integer> categoryIds,
        boolean autoSave) {
        SiteDetailVO updatedSite = createOrUpdate(siteToUpdate, tagIds, categoryIds);
        if (!autoSave) {
            // Log the creation
            LogEvent logEvent = new LogEvent(this, updatedSite.getId().toString(),
                LogType.SITE_EDITED, updatedSite.getName());
            eventPublisher.publishEvent(logEvent);
        }
        return updatedSite;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Site updateStatus(SiteStatus status, Integer siteId) {
        siteRepository.updateStatus(status, siteId);
        Site site = siteRepository.getById(siteId);
        eventPublisher.publishEvent(new SiteUpdatedEvent(this, site));
        return site;
    }

    @Override
    public Site getBy(SiteStatus status, String slug) {
        return siteRepository.getBySlugAndStatus(slug, status)
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @Override
    public Site getBy(Integer year, Integer month, String slug) {
        Assert.notNull(year, "Site create year must not be null");
        Assert.notNull(month, "Site create month must not be null");
        Assert.notNull(slug, "Site slug must not be null");

        Optional<Site> siteOptional = siteRepository.findBy(year, month, slug);

        return siteOptional
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @NonNull
    @Override
    public Site getBy(@NonNull Integer year, @NonNull String slug) {
        Assert.notNull(year, "Site create year must not be null");
        Assert.notNull(slug, "Site slug must not be null");

        Optional<Site> siteOptional = siteRepository.findBy(year, slug);

        return siteOptional
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @Override
    public Site getBy(Integer year, Integer month, String slug, SiteStatus status) {
        Assert.notNull(year, "Site create year must not be null");
        Assert.notNull(month, "Site create month must not be null");
        Assert.notNull(slug, "Site slug must not be null");
        Assert.notNull(status, "Site status must not be null");

        Optional<Site> siteOptional = siteRepository.findBy(year, month, slug, status);

        return siteOptional
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @Override
    public Site getBy(Integer year, Integer month, Integer day, String slug) {
        Assert.notNull(year, "Site create year must not be null");
        Assert.notNull(month, "Site create month must not be null");
        Assert.notNull(day, "Site create day must not be null");
        Assert.notNull(slug, "Site slug must not be null");

        Optional<Site> siteOptional = siteRepository.findBy(year, month, day, slug);

        return siteOptional
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @Override
    public Site getBy(Integer year, Integer month, Integer day, String slug, SiteStatus status) {
        Assert.notNull(year, "Site create year must not be null");
        Assert.notNull(month, "Site create month must not be null");
        Assert.notNull(day, "Site create day must not be null");
        Assert.notNull(slug, "Site slug must not be null");
        Assert.notNull(status, "Site status must not be null");

        Optional<Site> siteOptional = siteRepository.findBy(year, month, day, slug, status);

        return siteOptional
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Site> removeByIds(Collection<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return ids.stream().map(this::removeById).collect(Collectors.toList());
    }

    @Override
    public Site getBySlug(String slug) {
        return siteRepository.getBySlug(slug)
            .orElseThrow(() -> new NotFoundException("查询不到该网址的信息").setErrorData(slug));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Site removeById(Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        log.debug("Removing site: [{}]", siteId);

        // Remove site tags
        List<SiteTag> siteTags = siteTagService.removeBySiteId(siteId);

        log.debug("Removed site tags: [{}]", siteTags);

        // Remove site categories
        List<SiteCategory> siteCategories = siteCategoryService.removeBySiteId(siteId);

        log.debug("Removed site categories: [{}]", siteCategories);

        Site deletedSite = super.removeById(siteId);

        // Log it
        eventPublisher.publishEvent(new LogEvent(this, siteId.toString(), LogType.POST_DELETED,
            deletedSite.getName()));

        return deletedSite;
    }

    /**
     * Build specification by site query.
     *
     * @param siteQuery site query must not be null
     * @return a site specification
     */

    @NonNull
    private Specification<Site> buildSpecByQuery(@NonNull SiteQuery siteQuery) {
        Assert.notNull(siteQuery, "Site query must not be null");

        return (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new LinkedList<>();

            Set<SiteStatus> statuses = siteQuery.getStatuses();
            if (!CollectionUtils.isEmpty(statuses)) {
                predicates.add(root.get("status").in(statuses));
            }

            if (siteQuery.getCategoryId() != null) {
                List<Integer> categoryIds =
                    categoryService.listAllByParentId(siteQuery.getCategoryId())
                        .stream()
                        .map(Category::getId)
                        .collect(Collectors.toList());
                Subquery<Site> siteSubquery = query.subquery(Site.class);
                Root<SiteCategory> siteCategoryRoot = siteSubquery.from(SiteCategory.class);
                siteSubquery.select(siteCategoryRoot.get("siteId"));
                siteSubquery.where(
                    criteriaBuilder.equal(root.get("id"), siteCategoryRoot.get("siteId")),
                    siteCategoryRoot.get("categoryId").in(categoryIds));
                predicates.add(criteriaBuilder.exists(siteSubquery));
            }

            if (siteQuery.getKeyword() != null) {

                // Format like condition
                String likeCondition = String
                    .format("%%%s%%", StringUtils.strip(siteQuery.getKeyword()));

                // Build like predicate
                Subquery<Site> siteSubquery = query.subquery(Site.class);
                Root<Content> contentRoot = siteSubquery.from(Content.class);
                siteSubquery.select(contentRoot.get("id"))
                    .where(criteriaBuilder.like(contentRoot.get("rawContent"), likeCondition));

                Predicate nameLike = criteriaBuilder.like(root.get("name"), likeCondition);

                predicates.add(
                    criteriaBuilder.or(nameLike, criteriaBuilder.in(root).value(siteSubquery)));
            }

            return query.where(predicates.toArray(new Predicate[0])).getRestriction();
        };
    }

    private SiteDetailVO createOrUpdate(@NonNull Site site, Set<Integer> tagIds,
        Set<Integer> categoryIds) {
        Assert.notNull(site, "Site param must not be null");

        site = createOrUpdateBy(site);

        siteTagService.removeBySiteId(site.getId());

        siteCategoryService.removeBySiteId(site.getId());

        // List all tags
        List<Tag> tags = tagService.listAllByIds(tagIds);

        // List all categories
        List<Category> categories = categoryService.listAllByIds(categoryIds);

        // Create site tags
        List<SiteTag> siteTags = siteTagService.mergeOrCreateByIfAbsent(site.getId(),
            ServiceUtils.fetchProperty(tags, Tag::getId));

        log.debug("Created site tags: [{}]", siteTags);

        // Create site categories
        List<SiteCategory> siteCategories =
            siteCategoryService.mergeOrCreateByIfAbsent(site.getId(),
                ServiceUtils.fetchProperty(categories, Category::getId));

        log.debug("Created site categories: [{}]", siteCategories);

        // Publish site updated event.
        applicationContext.publishEvent(new SiteUpdatedEvent(this, site));

        // Convert to site detail vo
        return siteAssembler.convertTo(site, tags, categories);
    }

    @Override
    public void publishVisitEvent(Integer siteId) {
        eventPublisher.publishEvent(new SiteVisitEvent(this, siteId));
    }

    /**
     * frontMatter has a variety of parsing methods, but the most commonly used is the yaml type.
     * yaml is used here, and public methods can be extracted if needed for extensions.
     * <p>
     * Example: <br>
     * title: default title. <br>
     * date: 2022-04-03 19:00:00.000 <br>
     * updated: 2022-04-03 19:00:00.000 <br>
     * description: default description <br>
     * categories: <br>
     * - Java <br>
     * - Halo <br>
     * tags: <br>
     * - tag <br>
     * - doc <br>
     * </p>
     *
     * @param site site not be null
     * @return frontMatter
     */
    private StringBuilder getFrontMatterYaml(Site site) {
        StringBuilder frontMatter = new StringBuilder("---\n");
        frontMatter.append("name: ").append(site.getName()).append("\n");
        frontMatter.append("date: ").append(site.getCreateTime()).append("\n");
        frontMatter.append("updated: ").append(site.getUpdateTime()).append("\n");
        frontMatter.append("url: ").append(site.getUrl()).append("\n");

        // set category
        // classification with hierarchies has not been processed yet
        List<Category> categories = siteCategoryService.listCategoriesBy(site.getId());
        StringBuilder categoryContent = new StringBuilder();
        categories.forEach(category -> categoryContent.append("- ").append(category.getName())
            .append("\n"));
        frontMatter.append("categories: ").append("\n").append(categoryContent);

        // set tags
        List<Tag> tags = siteTagService.listTagsBy(site.getId());
        StringBuilder tagContent = new StringBuilder();
        tags.forEach(tag -> tagContent.append("- ").append(tag.getName()).append("\n"));
        frontMatter.append("tags: ").append("\n").append(tagContent);

        frontMatter.append("---\n");
        return frontMatter;
    }

    @Transactional(rollbackFor = Exception.class)
    public Site createOrUpdateBy(Site site) {
        Assert.notNull(site, "Site must not be null");
        Site savedSite;
        // Create or update site
        if (ServiceUtils.isEmptyId(site.getId())) {
            // The sheet will be created
            savedSite = create(site);
        } else {
            // Update it
            savedSite = update(site);
        }
        return savedSite;
    }

    @Override
    @Transactional
    public void increaseVisit(long visits, Integer siteId) {
        Assert.isTrue(visits > 0, "Visits to increase must not be less than 1");
        Assert.notNull(siteId, "Site id must not be null");

        boolean finishedIncrease;
        if (siteRepository.getByIdAndStatus(siteId, SiteStatus.PUBLISHED).isPresent()) {
            finishedIncrease = true;
            log.info("Site with id: [{}] is a draft and visits will not be updated", siteId);
        } else {
            finishedIncrease = siteRepository.updateVisit(visits, siteId) == 1;
        }

        if (!finishedIncrease) {
            log.error("Site with id: [{}] may not be found", siteId);
            throw new BadRequestException(
                "Failed to increase visits " + visits + " for site with id " + siteId);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void increaseVisit(Integer siteId) {
        increaseVisit(1L, siteId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void increaseLike(long likes, Integer siteId) {
        Assert.isTrue(likes > 0, "Likes to increase must not be less than 1");
        Assert.notNull(siteId, "Site id must not be null");

        long affectedRows = siteRepository.updateLikes(likes, siteId);

        if (affectedRows != 1) {
            log.error("Site with id: [{}] may not be found", siteId);
            throw new BadRequestException(
                "Failed to increase likes " + likes + " for site with id " + siteId);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void increaseLike(Integer siteId) {
        increaseLike(1L, siteId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Site> updateStatusByIds(List<Integer> ids, SiteStatus status) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return ids.stream().map(id -> updateStatus(status, id)).collect(Collectors.toList());
    }
}
